package com.hliushi.learn.config;

import com.hliushi.learn.annotation.Log;
import io.swagger.annotations.ApiOperation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * @ClassName LogAspectHandler
 * @Description 实现AOP切面
 * @Author hliushi
 * @Date 2021/7/7 9:49
 * @Version V1.0
 **/
@Aspect         // 用来描述一个切面类, 定义切面类的时候需要打上这个注解
@Component      // @Component注解让类交给Spring来管理
public class LogAspectHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(LogAspectHandler.class);

    /**
     * @Pointcut 定义一个切面, 即上面所描述的关注的某件事入口
     * @Before 在做某件事之前做的事
     * @After 在做某件事之后要做的事
     * @AfterReturning 在做某件事之后, 对其返回值做增强处理
     * @AfterThrowing 在做某件事抛出异常时, 处理
     */


    /**
     * 定义一个切面, 拦截com.hliushi.learn.controller包和子包下的所有方法
     * -- 通过定义切入点, 我们可以复用切点, 减少重复定义切点表达式
     * #
     * Pointcut注解: 用来定义一个切面(切入点), 即上文中所关注的某件事情的入口
     * .            切入点决定了连接点关注的内容, 使得我们可以控制通知什么时候执行
     * #
     * Pointcut注解指定一个切面, 定义需要拦截的东西, 这里介绍两种常用的表达式: execution() 和 annotation()
     * 以 execution(* com.hliushi.learn.controller..*.*(..)) 表达式为例 , 语法如下:
     * execution()为表示式主体
     * 第一个 * 号的位置 : 表示返回值类型, * 表示所有类型
     * 包名: 表示需要拦截的包名, 后面的两个句点表示当前包和当前包的所有子包,
     * .        com.hliushi.learn.controller包, 子包下所有类的方法
     * 第二个 * 号的位置 : 表示类名, * 表示所有类
     * *(..) : 这个星号表示方法名, * 表示所有的方法, 后面括号里面表示方法的参数, 两个句点表示任何参数
     * #
     * execution(* com.hliushi.learn.controller.AopController.*(..))
     * 拦截com.hliushi.learn.controllerAop.Controller类中下的所有方法
     */
    @Pointcut("execution(* com.hliushi.learn.controller.AopController.*(..))")
    // @Pointcut("execution(* com.hliushi.learn.controller.AopController.*.*(..))")  错误语法!!!
    public void pointCut() {

    }

    /**
     * annotation()方法是针对某个注解来定义切面, 比如我们对具有@GetMapping注解的方法作切面
     * 然后使用该切面的话, 就会切入注解是@GetMapping的方法, 因为咋实际项目中,
     * 可能对于不同的注解有不同的逻辑处理, 比如@GetMapping, @PostMapping, @DeleteMapping
     */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void annotationCut() {

    }


    /**
     * 在上面定义的切面方法之前执行该方法
     * #
     * Before 注解指定的方法在切面pointcut切入目标方法之前执行, 可以做一些log处理
     * 也可以做一些信息的统计, 比如获取用户的请求url以及用户的ip地址, 这个在做个人站点的时候都用得到
     *
     * @param joinPoint 它可以获取一个签名, 然后利用签名可以获取请求的包名, 方法名, 包括参数
     */
    @Before("pointCut()")
    public void deBefore(JoinPoint joinPoint) {
        LOGGER.info("===== deBefore方法进入了 =====");

        // 获取@Log注解
        Log controllerLog = this.getAnnotationLog(joinPoint);
        if (controllerLog == null) {
            return;
        }

        // 获取@ApiOperation注解
        ApiOperation apiOperation = this.getAnnotationApiOperation(joinPoint);

        // fix: 修复bug01
        // 打印出@Log注解上面的字段
        String module = controllerLog.module();
        // feat: 功能1
        /**
         * BusinessType()是一个枚举,
         * Enum.ordinal() 是枚举中方法, 返回次枚举常量的序数 (它在枚举声明中的位置, 其中初始常量被分配0序数)
         */
        int businessTypeOrdinal = controllerLog.businessType().ordinal();
        /**
         * 返回此枚举常量的名称，与在其枚举声明中声明的完全相同
         */
        String businessTypeName = controllerLog.businessType().name();

        String methodDesc = apiOperation == null ? controllerLog.methodDesc() : apiOperation.value();
        // String methodDesc;
        // if (apiOperation == null) {
        //     methodDesc = controllerLog.methodDesc();
        // } else {
        //     methodDesc = apiOperation.value();-       // }


        LOGGER.info("module = {}", module);
        LOGGER.info("businessTypeOrdinal = {}", businessTypeOrdinal);
        LOGGER.info("methodDesc = {}", methodDesc);
        LOGGER.info("businessTypeName = {}", businessTypeName);

        // 获取签名
        Signature signature = joinPoint.getSignature();
        // 获取切入的包名
        // String getDeclaringTypeName(); 返回声明类型的完全限定名称。
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取即将执行的方法名
        String funcName = signature.getName();
        LOGGER.info("即将执行方法为: {}, 属于{}包", funcName, declaringTypeName);

        String methodPath = declaringTypeName + "." + funcName + "()";
        LOGGER.info("methodPath = {}", methodPath);
        // 也可以用来记录一些信息, 比如获取请求的url和ip
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 获取请求controller的资源路径
        String apiPath = request.getRequestURI();
        // 获取请求url
        String url = request.getRequestURL().toString();
        // 获取请求ip
        String ip = request.getRemoteAddr();

        LOGGER.info("用户请求的url为: {}, ip地址为: {}", url, ip);
        LOGGER.info("API_PATH = {}", apiPath);
    }


    /**
     * 在上面定义的切面方法之后执行该方法
     * #
     * After注解和Before注解相对应, 指定的方法在切面切入目标方法之后执行, 也可以做一些完成某方法之后的log处理
     *
     * @param joinPoint
     */
    @After("pointCut()")
    public void doAfter(JoinPoint joinPoint) {

        LOGGER.info("===== deAfter方法进入了 =====");
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        LOGGER.info("方法 {} 已经执行完", method);
    }

    /**
     * 在上面定义的切面方法返回后执行该方法, 可以捕获返回对象或者对返回对象进行增强
     * #
     * AfterReturning注解和After有些类似, 区别在于AfterReturning注解可以用来捕获切入方法执行完之后的返回值
     * 对返回值进行业务逻辑上的增强处理
     * #
     * 注意:在@AfterReturning注解中, 属性returning的值必须要和参数保持一致, 否则会检测不到.
     * 该方法中的第二个入参就是被切方法的返回值, 在doAfterReturning方法中可以对返回值进行增强,
     * 可以根据业务需要做相应的封装
     *
     * @param joinPoint
     * @param result
     */
    @AfterReturning(pointcut = "pointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {
        Signature signature = joinPoint.getSignature();
        String classMethod = signature.getName();
        LOGGER.info("方法 {} 执行完毕, 返回参数为: {}", classMethod, result);

        // 实际项目中可以根据业务做具体的返回值增强
        LOGGER.info("对返回参数进行业务上的增强: {}", result + "增强版");
    }

    /**
     * .@AfterThrowing注解是当被切方法执行时抛出异常时, 会进入@AfterThrowing注解的方法中执行
     * 在该方法中可以做一些异常的处理逻辑
     * 需要注意的是throwing属性的值必须要和参数一致, 否则会报错, 该方法中的第二个入参即为抛出的异常
     *
     * @param joinPoint
     * @param ex
     */
    @AfterThrowing(pointcut = "pointCut()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
        Signature signature = joinPoint.getSignature();
        String classMethod = signature.getName();
        // 处理异常逻辑
        LOGGER.info("执行方法 {} 出错, 异常为: {}", classMethod, ex);
    }


    /**
     * 是否存在注解, 如果存在就获取
     *
     * @param joinPoint joinPoint
     * @return Log
     */
    private Log getAnnotationLog(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(Log.class);
        }
        return null;
    }

    /**
     * 是否存在注解, 如果存在就获取
     *
     * @param joinPoint joinPoint
     * @return ApiOperation
     */
    private ApiOperation getAnnotationApiOperation(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(ApiOperation.class);
        }
        return null;
    }
}
